#include "PixelCollision.h"
#include "Common/Macros.h"
#include "Common/Utils.h"

static const auto kVertexShader = "res/Shaders/SolidColorShader.vsh";
static const auto kFragmentShader = "res/Shaders/SolidColorShader.fsh";
static const auto kShaderRedUniform = "u_color_red";
static const auto kShaderBlueUniform = "u_color_blue";
static const auto kOpacityThreshold = 20;

PixelCollision* PixelCollision::s_instance = nullptr;

// Private Constructor being called from within the GetInstance handle
PixelCollision::PixelCollision(void) :
_glProgram(nullptr),
_rt(nullptr),
_pixelReader(nullptr) 
{
	_glProgram = GLProgram::createWithFilenames(kVertexShader, kFragmentShader);
	_glProgram->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_POSITION, GLProgram::VERTEX_ATTRIB_POSITION);
	_glProgram->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_TEX_COORD, GLProgram::VERTEX_ATTRIB_TEX_COORD);
	_glProgram->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_COLOR, GLProgram::VERTEX_ATTRIB_COLOR);

	const Size &size = Director::getInstance()->getWinSize();

	_rt = RenderTexture::create(size.width, size.height, Texture2D::PixelFormat::RGBA8888);
	_pixelReader = PixelReaderNode::create(Point::ZERO);

	_glProgram->retain();
	_rt->retain();
	_pixelReader->retain();
}

PixelCollision::~PixelCollision(void) 
{
	_glProgram->release();
	_rt->release();
	_pixelReader->release();
}

PixelCollision* PixelCollision::getInstance(void) 
{
	if (!s_instance) {
		s_instance = new PixelCollision();
	}
	return s_instance;
}

void PixelCollision::destroyInstance(void) 
{
	delete s_instance;
	s_instance = nullptr;
}

bool PixelCollision::collidesWithSprite(Sprite *sprite1, Sprite *sprite2) 
{
	return this->collidesWithSprite(sprite1, sprite2, true);
}

bool PixelCollision::collidesWithSprite(Sprite *sprite1, Sprite *sprite2, bool pp)
{
#ifdef DEBUG_COLLISION_TEST_VISUALIZATION
	// debug
	//
	Scene *cur_n = Director::getInstance()->getRunningScene();
	Sprite *s_blend = dynamic_cast<Sprite*>(cur_n->getChildByTag(11));
	if (s_blend) {
		s_blend->removeFromParent();
	}
	//
	// /debug
#endif

	Rect r1 = Rect(0, 0, sprite1->getContentSize().width, sprite1->getContentSize().height);
	r1 = RectApplyAffineTransform(r1, sprite1->getNodeToWorldAffineTransform());
	//SDEBUG(LEVEL_DEBUG, "dragging", "Collision test: r1_new=%s;", RectStr(r1));


	Rect r2 = Rect(0, 0, sprite2->getContentSize().width, sprite2->getContentSize().height);
	r2 = RectApplyAffineTransform(r2, sprite2->getNodeToWorldAffineTransform());
	//SDEBUG(LEVEL_DEBUG, "dragging", "Collision test: r2_new=%s;", RectStr(r2));

	if (r1.intersectsRect(r2)) 
	{
		if (!pp) 
		{
			//SDEBUG(LEVEL_DEBUG, "dragging", "Collision test (intersects/pixelPerfect): ACCEPTED by !pp; rect1=%s; rect2=%s;", RectStr(r1), RectStr(r2));
			return true;
		}

		Rect intersection = this->getIntersection(r1, r2);
		unsigned int numPixels = intersection.size.width * intersection.size.height;

		_rt->beginWithClear(0, 0, 0, 0);

		CustomCommand sprite1Command;
		CustomCommand sprite2Command;
		auto oldPosition1 = this->renderSprite(sprite1, sprite1Command, true);
		auto oldPosition2 = this->renderSprite(sprite2, sprite2Command, false);

#ifdef DEBUG_COLLISION_TEST_VISUALIZATION
		// debug
		//
		Node *n;
		n = this->drawRect(intersection, Color4F(255, 0, 0, 1));
		n->visit();
		//
		// /debug
#endif

		_pixelReader->setReadPoint(intersection.origin);
		_pixelReader->setReadSize(intersection.size);
		_pixelReader->reset();
		_pixelReader->visit();

		auto buffer = _pixelReader->getBuffer();
		_rt->end();
		Director::getInstance()->getRenderer()->render();

		this->resetSprite(sprite1, oldPosition1);
		this->resetSprite(sprite2, oldPosition2);

		unsigned int maxIndex = numPixels * 4;
		bool retVal = false;
		for (unsigned int i = 0; i < maxIndex; i += 4) 
		{
			if (buffer[i] > 0 && buffer[i + 2] > 0 && buffer[i + 3] > kOpacityThreshold) 
			{ // red and blue
				retVal = true;
				break;
			}
		}
		//SDEBUG(LEVEL_DEBUG, "dragging", "Collision test (intersects/pixelPerfect): retVal=%s; rect1=%s; rect2=%s; intersection=%s; numPixels=%d; CC_CONTENT_SCALE_FACTOR=%.2f; s1.scale=%.2f; s2.scale=%.2f;",
		//	BOOLStr(retVal), RectStr(r1), RectStr(r2), RectStr(intersection), numPixels, CC_CONTENT_SCALE_FACTOR(), sprite1->getScale(), sprite2->getScale());

#ifdef DEBUG_COLLISION_TEST_VISUALIZATION
		// debug
		//
		s_blend = CCSprite::createWithTexture(_rt->getSprite()->getTexture());
		s_blend->setTag(11);
		s_blend->setPosition(Vec2::ZERO);
		s_blend->setAnchorPoint(Vec2::ZERO);
		cur_n->addChild(s_blend);
		//
		// debug
#endif
		return retVal;
	}

	//SDEBUG(LEVEL_DEBUG, "dragging", "Collision test (intersects/pixelPerfect): REJECTED by intersectsRect(); rect1=%s; rect2=%s;", RectStr(r1), RectStr(r2));

	return false;
}

Node* PixelCollision::drawRect(Rect r, const Color4F &color)
{
	r.origin.x /= CC_CONTENT_SCALE_FACTOR();
	r.origin.y /= CC_CONTENT_SCALE_FACTOR();
	r.size.width /= CC_CONTENT_SCALE_FACTOR();
	r.size.height /= CC_CONTENT_SCALE_FACTOR();

	Size winSize = r.size;
	Point pos = r.origin;
	auto rectNode = DrawNode::create();
	Vec2 rectangle[4];
	rectangle[0] = Vec2(pos.x + 0, pos.y + 0);
	rectangle[1] = Vec2(pos.x + winSize.width, pos.y + 0);
	rectangle[2] = Vec2(pos.x + winSize.width, pos.y + winSize.height);
	rectangle[3] = Vec2(pos.x + 0, pos.y + winSize.height);

	rectNode->drawPolygon(rectangle, 4, Color4F(255, 0, 0, 255), 1, color);
	return rectNode;
}

bool PixelCollision::collidesWithPoint(Sprite *sprite, const Point &point) 
{
	_rt->beginWithClear(0, 0, 0, 0);
	glColorMask(1, 0, 0, 1);

	Point oldPosition = sprite->getPosition();
	//sprite->setPosition(sprite->getParent()->convertToWorldSpace(oldPosition));
	sprite->setPosition(sprite->getParent()->convertToWorldSpace(oldPosition) / sprite->getParent()->getScale());
	sprite->visit();

	//auto readPoint = sprite->getParent()->convertToWorldSpace(point) * CC_CONTENT_SCALE_FACTOR();
	auto readPoint = point * CC_CONTENT_SCALE_FACTOR() / sprite->getParent()->getScale();
	_pixelReader->setReadPoint(readPoint);
	_pixelReader->setReadSize(Size(1, 1));
	_pixelReader->visit();

	auto color = _pixelReader->getBuffer();
	_rt->end();
	Director::getInstance()->getRenderer()->render();
	glColorMask(1, 1, 1, 1);
	sprite->setPosition(oldPosition);

	//DEBUG_LOG("'%s'(%.2f,%.2f), size = (%.2f, %.2f), point = (%.2f, %.2f), Alpha = %d", 
	//	sprite->getName().c_str(), sprite->getPosition().x, sprite->getPosition().y,
	//	_rt->getSprite()->getContentSize().width, _rt->getSprite()->getContentSize().height,
	//	point.x, point.y, color[0]);

	return color[0] > 0;
}

// Private methods
PixelCollision::PixelReaderNode *PixelCollision::PixelReaderNode::create(const Point &readPoint) 
{
	auto pixelReader = new PixelReaderNode(readPoint);
	if (pixelReader && pixelReader->init()) {
		pixelReader->autorelease();
		return pixelReader;
	}
	CC_SAFE_DELETE(pixelReader);
	return nullptr;
}

PixelCollision::PixelReaderNode::PixelReaderNode(const Point &readPoint) :
_readPoint(readPoint),
_readSize(Size::ZERO),
_buffer(nullptr) 
{
	this->setReadSize(Size(1, 1));
}

PixelCollision::PixelReaderNode::~PixelReaderNode(void) 
{
	free(_buffer);
}

void PixelCollision::PixelReaderNode::draw(Renderer *renderer, const Mat4& transform, uint32_t flags) 
{
	_readPixelCommand.init(_globalZOrder);
	_readPixelCommand.func = CC_CALLBACK_0(PixelCollision::PixelReaderNode::onDraw, this);
	renderer->addCommand(&_readPixelCommand);
}

void PixelCollision::PixelReaderNode::onDraw(void) 
{
	glReadPixels(_readPoint.x, _readPoint.y, _readSize.width, _readSize.height,
		GL_RGBA, GL_UNSIGNED_BYTE, _buffer);
}

Rect PixelCollision::getIntersection(const Rect &r1, const Rect &r2) const
{
	// <Pp>
	Rect intersection;
	intersection = Rect(std::max(r1.getMinX(), r2.getMinX()), std::max(r1.getMinY(), r2.getMinY()), 0, 0);

	intersection.size.width = std::min(r1.getMaxX(), r2.getMaxX()) - intersection.getMinX();
	intersection.size.height = std::min(r1.getMaxY(), r2.getMaxY()) - intersection.getMinY();

	intersection.origin.x *= CC_CONTENT_SCALE_FACTOR();
	intersection.origin.y *= CC_CONTENT_SCALE_FACTOR();
	intersection.size.width *= CC_CONTENT_SCALE_FACTOR();
	intersection.size.height *= CC_CONTENT_SCALE_FACTOR();

	return intersection;
	// </Pp>

	/* <ORIG>
	float tempX;
	float tempY;
	float tempWidth;
	float tempHeight;

	if (r1.getMaxX() > r2.getMinX()) {
	tempX = r2.getMinX();
	tempWidth = r1.getMaxX() - r2.getMinX();
	} else {
	tempX = r1.getMinX();
	tempWidth = r2.getMaxX() - r1.getMinX();
	}
	if (r2.getMaxY() < r1.getMaxY()) {
	tempY = r1.getMinY();
	tempHeight = r2.getMaxY() - r1.getMinY();
	} else {
	tempY = r2.getMinY();
	tempHeight = r1.getMaxY() - r2.getMinY();
	}

	return Rect(tempX * CC_CONTENT_SCALE_FACTOR(), tempY * CC_CONTENT_SCALE_FACTOR(),
	tempWidth * CC_CONTENT_SCALE_FACTOR(), tempHeight * CC_CONTENT_SCALE_FACTOR());*/
}

Point PixelCollision::renderSprite(Sprite *sprite, CustomCommand &command, bool red) 
{
	command.init(sprite->getGlobalZOrder());
	command.func = [=]() 
	{
		sprite->getGLProgramState()->setUniformInt(kShaderRedUniform, red ? 255 : 0);
		sprite->getGLProgramState()->setUniformInt(kShaderBlueUniform, red ? 0 : 255);
	};
	Director::getInstance()->getRenderer()->addCommand(&command);

	sprite->setGLProgram(_glProgram);
	sprite->setBlendFunc(BlendFunc::ADDITIVE);
	Point oldPosition = sprite->getPosition();
	sprite->setPosition(sprite->getParent()->convertToWorldSpace(oldPosition));
	sprite->visit();

	return oldPosition;
}

void PixelCollision::resetSprite(Sprite *sprite, const Point &oldPosition) 
{
	auto program = ShaderCache::getInstance()->getGLProgram(
		GLProgram::SHADER_NAME_POSITION_TEXTURE_COLOR_NO_MVP);
	sprite->setGLProgram(program);
	sprite->setBlendFunc(BlendFunc::ALPHA_PREMULTIPLIED);
	sprite->setPosition(oldPosition);
}
